/*	
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { AstTreeUtils, ArkFile, ts } from "arkanalyzer";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Rule, Defects, FileMatcher, MatcherTypes, MatcherCallback } from '../../Index';
import { RuleListUtil } from "../../utils/common/DefectsList";
import { IssueReport } from "../../model/Defects";
let optionIncludeExports = false;
interface RuleOption {
    includeExports?: boolean;
}
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-duplicate-imports-check.md',
    description: '"module" import is duplicated.  (no-duplicate-imports)'
};
let filePath = '';
export class NoDuplicateImportsCheck implements BaseChecker {
    private importedModules: Map<string, ts.Node[]> = new Map();
    private namespaceImportsExports: Map<string, ts.Node[]> = new Map();
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private buildMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };
    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check.bind(this)
        };
        return [matchBuildCb];
    };
    public check = (arkFile: ArkFile) => {
        filePath = arkFile.getFilePath();
        const ruleOption = this.rule.option?.[0] as RuleOption | undefined;
        optionIncludeExports = ruleOption?.includeExports ?? false;
        this.importedModules.clear();
        this.namespaceImportsExports.clear();
        const astRoot = AstTreeUtils.getASTNode(arkFile.getName(), arkFile.getCode());
        for (let child of astRoot.statements) {
            this.checkImportDeclaration(child);
        };
    };
    private checkImportDeclaration = (node: ts.Node): void => {
        const processModuleSpecifier = (node: ts.ImportDeclaration | ts.ExportDeclaration, isImport: boolean): void => {
            const moduleSpecifier = node.moduleSpecifier;
            if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
                const moduleName = moduleSpecifier.text;
                const action = isImport ? 'import' : 'export';
                const asimport = isImport ? '' : ' as import';
                const errorMessage = `'${moduleName}' ${action} is duplicated${asimport}. (no-duplicate-imports)`;
                this.handleImportExportStatement(node, moduleName, errorMessage, isImport);
            }
        };
        if (ts.isImportDeclaration(node)) {
            processModuleSpecifier(node, true);
        } else if (optionIncludeExports && ts.isExportDeclaration(node)) {
            processModuleSpecifier(node, false);
        };
        ts.forEachChild(node, this.checkImportDeclaration);
    };
    private handleImportExportStatement(node: ts.ImportDeclaration | ts.ExportDeclaration, moduleName: string, errorMessage: string, isImport: boolean): void {
        const alias = this.getNamespaceAlias(node);
        const key = `${moduleName}|${alias || ''}`;
        if (alias) {
            this.checkAndStore(isImport ? node as ts.ImportDeclaration : node as ts.ExportDeclaration, key, this.namespaceImportsExports, errorMessage);
        } else {
            this.checkAndStore(isImport ? node as ts.ImportDeclaration : node as ts.ExportDeclaration, moduleName, this.importedModules, errorMessage);
        };
    };
    private checkAndStore(node: ts.Node, key: string, storage: Map<string, ts.Node[]>, errorMessage: string): void {
        if (storage.has(key)) {
            const previousNodes = storage.get(key)!;
            if (this.shouldReportImportExport(node, previousNodes)) {
                this.addIssueReport(node, errorMessage, this.metaData.severity);
            };
        };
        storage.set(key, [...storage.get(key) || [], node]);
    };
    private shouldReportImportExport(node: ts.Node, previousNodes: ts.Node[]): boolean {
        return previousNodes.some(prevNode => {
            if (ts.isImportDeclaration(prevNode) && ts.isImportDeclaration(node)) {
                return true;
            };
            if (optionIncludeExports && ts.isExportDeclaration(prevNode) && ts.isExportDeclaration(node)) {
                return true;
            };
            if (optionIncludeExports && ts.isImportDeclaration(prevNode) && ts.isExportDeclaration(node)) {
                return true;
            };
            if (optionIncludeExports && ts.isExportDeclaration(prevNode) && ts.isImportDeclaration(node)) {
                return true;
            }
            return false;
        });
    };
    private getNamespaceAlias(node: ts.ImportDeclaration | ts.ExportDeclaration): string | undefined {
        if (ts.isImportDeclaration(node)) {
            const namedBindings = node.importClause?.namedBindings;
            if (namedBindings && ts.isNamespaceImport(namedBindings)) {
                return namedBindings.name.text;
            };
        } else if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamespaceExport(node.exportClause)) {
            return node.exportClause.name.text;
        };
        return undefined;
    };
    private addIssueReport(node: ts.Node, description: string, severity: number): void {
        const warnInfo = this.getLineAndColumn(node);
        const defect = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, description, severity,
            this.rule.ruleId, warnInfo.filePath, this.metaData.ruleDocPath, true, false, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    };
    private getLineAndColumn(node: ts.Node): { line: number; startCol: number; endCol: number; filePath: string } {
        const sourceFile = node.getSourceFile();
        const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
        const endCharacter = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).character;
        return {
            line: line + 1,
            startCol: character + 1,
            endCol: endCharacter + 1,
            filePath: filePath
        };
    };
};
